ATOM SaaS Test Writing Guide
**Version:** 1.0
**Last Updated:** 2026-02-22
**Purpose:** Templates and examples for writing unit, integration, and E2E tests
---
Table of Contents
- Unit Test Template (Frontend)
- Unit Test Template (Backend)
- Integration Test Template
- E2E Test Template
- Test Fixtures Guide
- Mock Strategy Guide
---
1. Unit Test Template (Frontend)
Frontend unit tests use **Vitest** with **React Testing Library** for component testing.
Example: Agent Governance Unit Test
// src/lib/ai/__tests__/agent-governance.unit.test.ts
/**
* Agent Governance Unit Tests
*
* Tests for agent maturity level permissions, rate limiting, and governance validation.
* Uses vi.mock for DatabaseService and Redis dependencies.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AgentGovernanceService, MaturityLevel, ACTION_COMPLEXITY } from '../agent-governance';
import type { DatabaseService } from '../../database';
/**
* Mock DatabaseService for agent governance tests
*/
class MockDatabaseService {
private agents: Map<string, any> = new Map();
private users: Map<string, any> = new Map();
queryHistory: string[] = [];
constructor() {
// Initialize with test data
this.agents.set('agent-student', {
id: 'agent-student',
tenant_id: 'tenant-123',
name: 'Student Agent',
maturity_level: 'student' as MaturityLevel,
category: 'general',
confidence_score: 0.4,
});
this.agents.set('agent-autonomous', {
id: 'agent-autonomous',
tenant_id: 'tenant-123',
name: 'Autonomous Agent',
maturity_level: 'autonomous' as MaturityLevel,
category: 'finance',
confidence_score: 0.95,
});
}
setAgent(agentId: string, agent: any) {
this.agents.set(agentId, agent);
}
async query(sql: string, params?: any[]): Promise<any> {
this.queryHistory.push(sql);
// Handle SELECT queries
if (sql.includes('SELECT') && sql.includes('agent_registry')) {
const agentId = params?.[0];
return this.agents.get(agentId) || null;
}
return null;
}
}
describe('AgentGovernanceService', () => {
let governance: AgentGovernanceService;
let mockDb: MockDatabaseService;
beforeEach(() => {
mockDb = new MockDatabaseService();
governance = new AgentGovernanceService(mockDb as any);
});
describe('canPerformAction - Student Maturity', () => {
it('should allow read-only actions', async () => {
// Arrange
const tenantId = 'tenant-123';
const agentId = 'agent-student';
const action = 'read';
// Act
const decision = await governance.canPerformAction(tenantId, agentId, action);
// Assert
expect(decision.allowed).toBe(true);
expect(decision.reason).toBeNull();
});
it('should block high-risk actions', async () => {
// Arrange
const tenantId = 'tenant-123';
const agentId = 'agent-student';
const action = 'delete_code';
// Act
const decision = await governance.canPerformAction(tenantId, agentId, action);
// Assert
expect(decision.allowed).toBe(false);
expect(decision.reason).toContain('Insufficient maturity');
expect(decision.required_maturity).toBe('autonomous');
});
it('should allow search actions', async () => {
// Arrange
const tenantId = 'tenant-123';
const agentId = 'agent-student';
const action = 'search';
// Act
const decision = await governance.canPerformAction(tenantId, agentId, action);
// Assert
expect(decision.allowed).toBe(true);
expect(decision.action_complexity).toBe(1);
});
});
describe('canPerformAction - Autonomous Maturity', () => {
it('should allow all actions including high-risk', async () => {
// Arrange
const tenantId = 'tenant-123';
const agentId = 'agent-autonomous';
const action = 'delete_code';
// Act
const decision = await governance.canPerformAction(tenantId, agentId, action);
// Assert
expect(decision.allowed).toBe(true);
expect(decision.action_complexity).toBe(4);
expect(decision.reason).toBeNull();
});
it('should verify tenant isolation', async () => {
// Arrange
const tenant1 = 'tenant-123';
const tenant2 = 'tenant-456';
const agentId = 'agent-autonomous';
const action = 'delete_code';
// Act
const decision1 = await governance.canPerformAction(tenant1, agentId, action);
const decision2 = await governance.canPerformAction(tenant2, agentId, action);
// Assert - same agent ID for different tenants
expect(decision1.allowed).toBe(true);
expect(decision2.allowed).toBe(false); // Agent doesn't exist for tenant2
});
});
describe('getActionComplexity', () => {
it('should return correct complexity for known actions', () => {
expect(governance.getActionComplexity('read')).toBe(1);
expect(governance.getActionComplexity('delete_code')).toBe(4);
expect(governance.getActionComplexity('send_email')).toBe(3);
});
it('should return default complexity for unknown actions', () => {
const complexity = governance.getActionComplexity('unknown_action');
expect(complexity).toBe(2); // Default medium complexity
});
});
});Example: Component Unit Test
// src/components/ui/__tests__/button.test.tsx
/**
* Button Component Unit Tests
*
* Tests Button component rendering, interactions, and accessibility.
*/
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../button';
describe('Button Component', () => {
it('should render with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('should call onClick when clicked', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should be disabled when disabled prop is true', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick} disabled>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(screen.getByRole('button')).toBeDisabled();
expect(handleClick).not.toHaveBeenCalled();
});
it('should have accessible name', () => {
render(<Button aria-label="Close dialog">X</Button>);
expect(screen.getByRole('button', { name: 'Close dialog' })).toBeInTheDocument();
});
});Example: API Route Unit Test
// src/app/api/agents/__tests__/route.test.ts
/**
* Agents API Route Unit Tests
*
* Tests agent CRUD API endpoints with mocked database.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { POST } from '../route';
import { DatabaseService } from '@/lib/database';
// Mock database
vi.mock('@/lib/database');
describe('POST /api/agents', () => {
let mockDb: DatabaseService;
beforeEach(() => {
mockDb = {
query: vi.fn(),
} as any;
});
it('should create agent with valid input', async () => {
// Arrange
const requestBody = {
name: 'Test Agent',
maturity: 'supervised',
category: 'finance',
};
vi.mocked(mockDb.query).mockResolvedValue({
rows: [{ id: 'agent-123', ...requestBody }],
});
const request = new Request('http://localhost:3000/api/agents', {
method: 'POST',
body: JSON.stringify(requestBody),
});
// Act
const response = await POST(request);
const data = await response.json();
// Assert
expect(response.status).toBe(201);
expect(data.agent.id).toBe('agent-123');
expect(data.agent.name).toBe('Test Agent');
expect(mockDb.query).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO agent_registry'),
expect.arrayContaining([expect.any(String), 'Test Agent', 'supervised'])
);
});
it('should return 400 for missing required fields', async () => {
// Arrange
const requestBody = {
name: 'Test Agent',
// Missing maturity and category
};
const request = new Request('http://localhost:3000/api/agents', {
method: 'POST',
body: JSON.stringify(requestBody),
});
// Act
const response = await POST(request);
const data = await response.json();
// Assert
expect(response.status).toBe(400);
expect(data.error).toContain('required');
});
it('should verify tenant isolation', async () => {
// Arrange - tenant header
const requestBody = {
name: 'Test Agent',
maturity: 'supervised',
category: 'finance',
};
const request = new Request('http://localhost:3000/api/agents', {
method: 'POST',
headers: {
'X-Tenant-ID': 'tenant-123',
},
body: JSON.stringify(requestBody),
});
vi.mocked(mockDb.query).mockResolvedValue({
rows: [{ id: 'agent-123', tenant_id: 'tenant-123', ...requestBody }],
});
// Act
const response = await POST(request);
const data = await response.json();
// Assert
expect(data.agent.tenant_id).toBe('tenant-123');
expect(mockDb.query).toHaveBeenCalledWith(
expect.stringContaining('tenant_id'),
expect.arrayContaining([expect.any(String), 'Test Agent', expect.any(String), 'tenant-123'])
);
});
});---
2. Unit Test Template (Backend)
Backend unit tests use **pytest** with **fixtures** for test data generation.
Example: Agent Governance Unit Test (Python)
# backend-saas/tests/unit/test_agent_governance_service.py
"""
Agent Governance Service Unit Tests
Tests for agent maturity level permissions, rate limiting, and governance validation.
Uses pytest fixtures for test data and mock database.
"""
import pytest
from core.agent_governance_service import AgentGovernanceService, MaturityLevel
from unittest.mock import Mock, AsyncMock
@pytest.fixture
def mock_db():
"""Mock database service."""
db = Mock()
db.query = Mock()
return db
@pytest.fixture
def governance_service(mock_db):
"""Agent governance service instance."""
return AgentGovernanceService(mock_db)
@pytest.fixture
def test_agent():
"""Test agent data."""
return {
'id': 'agent-123',
'tenant_id': 'tenant-456',
'name': 'Test Agent',
'maturity_level': 'supervised',
'category': 'finance',
'confidence_score': 0.85
}
class TestAgentGovernanceService:
"""Agent governance service test cases."""
def test_student_agent_can_perform_read_only_actions(self, governance_service, test_agent, mock_db):
"""Given: Student maturity level
When: Checking read action permission
Then: Decision should allow the action
"""
# Arrange
test_agent['maturity_level'] = 'student'
action = 'read'
tenant_id = 'tenant-456'
mock_db.query.return_value.fetchone.return_value = test_agent
# Act
decision = governance_service.can_perform_action(
tenant_id,
test_agent['id'],
action
)
# Assert
assert decision.allowed is True
assert decision.reason is None
assert decision.action_complexity == 1
def test_student_agent_cannot_perform_high_risk_actions(self, governance_service, test_agent, mock_db):
"""Given: Student maturity level
When: Checking delete_code action permission
Then: Decision should block the action with reason
"""
# Arrange
test_agent['maturity_level'] = 'student'
action = 'delete_code'
tenant_id = 'tenant-456'
mock_db.query.return_value.fetchone.return_value = test_agent
# Act
decision = governance_service.can_perform_action(
tenant_id,
test_agent['id'],
action
)
# Assert
assert decision.allowed is False
assert 'Insufficient maturity' in decision.reason
assert decision.required_maturity == 'autonomous'
assert decision.action_complexity == 4
def test_autonomous_agent_can_perform_all_actions(self, governance_service, test_agent, mock_db):
"""Given: Autonomous maturity level
When: Checking high-risk action permission
Then: Decision should allow all actions
"""
# Arrange
test_agent['maturity_level'] = 'autonomous'
action = 'delete_code'
tenant_id = 'tenant-456'
mock_db.query.return_value.fetchone.return_value = test_agent
# Act
decision = governance_service.can_perform_action(
tenant_id,
test_agent['id'],
action
)
# Assert
assert decision.allowed is True
assert decision.reason is None
assert decision.action_complexity == 4
def test_get_action_complexity_returns_correct_values(self, governance_service):
"""Given: Known action types
When: Getting action complexity
Then: Should return correct complexity level
"""
# Act & Assert
assert governance_service.get_action_complexity('read') == 1
assert governance_service.get_action_complexity('search') == 1
assert governance_service.get_action_complexity('analyze') == 2
assert governance_service.get_action_complexity('create') == 3
assert governance_service.get_action_complexity('delete_code') == 4
def test_get_action_complexity_returns_default_for_unknown(self, governance_service):
"""Given: Unknown action type
When: Getting action complexity
Then: Should return default medium complexity
"""
# Act
complexity = governance_service.get_action_complexity('unknown_action')
# Assert
assert complexity == 2 # Default medium complexity
class TestTenantIsolation:
"""Tenant isolation test cases."""
def test_agent_isolated_by_tenant(self, governance_service, mock_db):
"""Given: Same agent ID for different tenants
When: Checking permissions
Then: Should query with tenant_id filter
"""
# Arrange
tenant1_id = 'tenant-123'
tenant2_id = 'tenant-456'
agent_id = 'agent-shared-id'
action = 'read'
# Act
governance_service.can_perform_action(tenant1_id, agent_id, action)
governance_service.can_perform_action(tenant2_id, agent_id, action)
# Assert - Both queries should include tenant_id filter
assert mock_db.query.call_count == 2
for call in mock_db.query.call_args_list:
query = call[0][0]
assert 'tenant_id' in query.lower() or '$1' in query
def test_cannot_access_agent_from_different_tenant(self, governance_service, mock_db):
"""Given: Agent from tenant-123
When: tenant-456 tries to access it
Then: Query should return None (agent not found)
"""
# Arrange
tenant_id = 'tenant-456'
agent_id = 'agent-123' # Belongs to tenant-123
action = 'read'
# Mock query to return None (agent doesn't exist for this tenant)
mock_db.query.return_value.fetchone.return_value = None
# Act
decision = governance_service.can_perform_action(tenant_id, agent_id, action)
# Assert
assert decision.allowed is False
assert 'not found' in decision.reason.lower()Example: Graduation Exam Unit Test
# backend-saas/tests/unit/test_graduation_exam.py
"""
Graduation Exam Service Unit Tests
Tests for graduation exam execution, readiness calculation, and validation.
"""
import pytest
from datetime import datetime, timedelta
from core.graduation_exam import GraduationExamService
from core.models import Episode, Agent
@pytest.fixture
def mock_db():
"""Mock database session."""
db = Mock()
db.query = Mock()
return db
@pytest.fixture
def exam_service(mock_db):
"""Graduation exam service instance."""
return GraduationExamService(mock_db)
@pytest.fixture
def test_episodes():
"""Test episode data for readiness calculation."""
base_time = datetime.utcnow()
return [
Episode(
id='episode-1',
agent_id='agent-123',
task_description='Task 1',
outcome='success',
success=True,
constitutional_violations=[],
zero_intervention=True,
created_at=base_time - timedelta(hours=5)
),
Episode(
id='episode-2',
agent_id='agent-123',
task_description='Task 2',
outcome='success',
success=True,
constitutional_violations=['minor_safety_issue'],
zero_intervention=False,
created_at=base_time - timedelta(hours=4)
),
Episode(
id='episode-3',
agent_id='agent-123',
task_description='Task 3',
outcome='failure',
success=False,
constitutional_violations=[],
zero_intervention=True,
created_at=base_time - timedelta(hours=3)
),
]
class TestGraduationReadiness:
"""Graduation readiness calculation tests."""
def test_calculate_readiness_for_intern_to_supervised(self, exam_service, test_episodes):
"""Given: 30 episodes with mixed outcomes
When: Calculating readiness for intern -> supervised
Then: Should return readiness score >= 0.80 threshold
"""
# Arrange
agent_id = 'agent-123'
target_maturity = 'supervised'
episode_count = 30
# Mock episodes query
exam_service.db.query.return_value.filter.return_value.limit.return_value.all.return_value = test_episodes * 10
# Act
readiness = exam_service.calculate_readiness(agent_id, target_maturity, episode_count)
# Assert
assert 0.0 <= readiness.readiness_score <= 1.0
assert readiness.zero_intervention_ratio >= 0.0
assert readiness.avg_constitutional_score >= 0.0
assert readiness.episode_count == 30
# Verify threshold: intern -> supervised requires 80% readiness
if readiness.readiness_score >= 0.80:
assert readiness.can_graduate is True
def test_readiness_requires_min_30_episodes(self, exam_service):
"""Given: Less than 30 episodes
When: Calculating readiness
Then: Should raise error for insufficient episodes
"""
# Arrange
agent_id = 'agent-123'
target_maturity = 'supervised'
episode_count = 15 # Below minimum
# Act & Assert
with pytest.raises(ValueError, match='at least 30 episodes'):
exam_service.calculate_readiness(agent_id, target_maturity, episode_count)
class TestGraduationExam:
"""Graduation exam execution tests."""
@pytest.mark.asyncio
async def test_execute_exam_success(self, exam_service, mock_db):
"""Given: Agent with 90% readiness
When: Executing graduation exam
Then: Should pass and update agent maturity
"""
# Arrange
agent_id = 'agent-123'
target_maturity = 'supervised'
# Mock readiness calculation
exam_service.calculate_readiness = Mock(return_value=Mock(
readiness_score=0.90,
can_graduate=True,
zero_intervention_ratio=0.85,
avg_constitutional_score=95.0,
episode_count=30
))
# Mock agent update
mock_db.query.return_value.filter.return_value.first.return_value = Mock(
id=agent_id,
maturity_level='intern'
)
# Act
result = await exam_service.execute_exam(agent_id, target_maturity, 30)
# Assert
assert result.passed is True
assert result.readiness_score == 0.90
assert result.new_maturity == 'supervised'
assert result.previous_maturity == 'intern'
@pytest.mark.asyncio
async def test_execute_exam_failure_below_threshold(self, exam_service):
"""Given: Agent with 70% readiness (below 80% threshold)
When: Executing graduation exam
Then: Should fail and not update maturity
"""
# Arrange
agent_id = 'agent-123'
target_maturity = 'supervised'
# Mock readiness below threshold
exam_service.calculate_readiness = Mock(return_value=Mock(
readiness_score=0.70,
can_graduate=False,
zero_intervention_ratio=0.60,
avg_constitutional_score=85.0,
episode_count=30
))
# Act
result = await exam_service.execute_exam(agent_id, target_maturity, 30)
# Assert
assert result.passed is False
assert result.readiness_score == 0.70
assert result.failure_reason contains('below threshold')---
3. Integration Test Template
Integration tests verify interactions between components (e.g., service + database).
Example: Hosting Integration Test
# backend-saas/tests/integration/test_hosting_integration.py
"""
Hosting Service Integration Tests
Tests ATOM Cloud hosting provisioning with mocked atom-cli.
"""
import pytest
from unittest.mock import patch, AsyncMock
from core.hosting_service import HostingService
from core.models import HostingProject, Tenant
@pytest.fixture
def mock_db_session():
"""Mock database session."""
session = Mock()
session.add = Mock()
session.commit = Mock()
session.flush = Mock()
session.refresh = Mock()
return session
@pytest.fixture
def test_tenant():
"""Test tenant data."""
return Tenant(
id='tenant-123',
subdomain='testcompany',
plan_tier='Team'
)
@pytest.fixture
def hosting_service(mock_db_session):
"""Hosting service instance."""
return HostingService(mock_db_session)
class TestHostingProvisioning:
"""Hosting provisioning integration tests."""
@pytest.mark.asyncio
@patch('subprocess.run')
async def test_provision_tenant_app_success(self, mock_run, hosting_service, test_tenant, mock_db_session):
"""Given: Team tenant with governance approval
When: Provisioning ATOM Cloud app
Then: Should create app, configure resources, return success
"""
# Arrange
project_name = 'test-app'
mock_run.return_value = Mock(
stdout=b'App created successfully',
stderr=b'',
returncode=0
)
# Mock governance check
with patch.object(hosting_service, '_check_governance_approval') as mock_gov:
mock_gov.return_value = True
# Act
result = await hosting_service.provision_tenant_app(
test_tenant.id,
project_name
)
# Assert
assert result.success is True
assert result.app_name.startswith(f'{test_tenant.subdomain}-{project_name}')
assert result.app_url is not None
# Verify atom-cli commands were called
assert mock_run.call_count >= 2 # create + config commands
# Verify database record created
assert mock_db_session.add.called
assert mock_db_session.commit.called
@pytest.mark.asyncio
@patch('subprocess.run')
async def test_provision_enforces_quota_limits(self, mock_run, hosting_service, test_tenant):
"""Given: Free tier tenant with 1 app limit
When: Provisioning second app
Then: Should reject with quota exceeded error
"""
# Arrange
project_name = 'second-app' # Already has 1 app
# Mock existing app count
with patch.object(hosting_service, '_get_app_count') as mock_count:
mock_count.return_value = 1 # At limit
# Act & Assert
with pytest.raises(ValueError, match='quota exceeded'):
await hosting_service.provision_tenant_app(
test_tenant.id,
project_name
)
@pytest.mark.asyncio
@patch('subprocess.run')
async def test_provision_fails_on_atom_cli_error(self, mock_run, hosting_service, test_tenant):
"""Given: ATOM Cloud CLI error
When: Provisioning app
Then: Should handle error gracefully, rollback database transaction
"""
# Arrange
project_name = 'test-app'
mock_run.return_value = Mock(
stdout=b'',
stderr=b'Error: Authentication required',
returncode=1 # Non-zero = error
)
# Act & Assert
with pytest.raises(Exception, match='ATOM Cloud provisioning failed'):
await hosting_service.provision_tenant_app(
test_tenant.id,
project_name
)
# Verify rollback was called
assert hosting_service.db.rollback.called
class TestCostEstimation:
"""Cost estimation integration tests."""
def test_estimate_cost_returns_accurate_pricing(self, hosting_service):
"""Given: Resource configuration
When: Estimating monthly cost
Then: Should return accurate cost breakdown
"""
# Arrange
cpu = 'dedicated-cpu-1x'
memory_gb = 2
storage_gb = 10
# Act
estimate = hosting_service.estimate_cost(cpu, memory_gb, storage_gb)
# Assert
assert estimate.cpu_cost_per_hour > 0
assert estimate.memory_cost_per_hour > 0
assert estimate.storage_cost_per_month > 0
assert estimate.total_monthly_cost > 0
# Verify calculation formula
expected_monthly = (
(estimate.cpu_cost_per_hour * 24 * 30) +
(estimate.memory_cost_per_hour * 24 * 30) +
estimate.storage_cost_per_month
)
assert abs(estimate.total_monthly_cost - expected_monthly) < 0.01---
4. E2E Test Template
E2E tests verify complete user workflows using **Playwright**.
Example: Complete Agent Workflow E2E Test
// tests/e2e/agent-workflow.test.ts
/**
* Agent Workflow E2E Tests
*
* Tests complete user journey from signup to agent execution.
*/
import { test, expect } from '@playwright/test';
test.describe('Agent Workflow', () => {
test.beforeEach(async ({ page }) => {
// Navigate to app
await page.goto('/');
// Login (or signup if needed)
await page.click('[data-testid="login-button"]');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.click('[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should create agent and execute skill successfully', async ({ page }) => {
// Navigate to agents page
await page.click('[data-testid="agents-nav"]');
await page.waitForURL('/agents');
// Create new agent
await page.click('[data-testid="create-agent"]');
await page.fill('[name="name"]', 'Test Finance Agent');
await page.selectOption('[name="maturity"]', 'supervised');
await page.selectOption('[name="category"]', 'finance');
await page.click('[data-testid="save-agent"]');
// Verify agent created
await expect(page.locator('text=Test Finance Agent')).toBeVisible();
await expect(page.locator('[data-testid="agent-maturity"]')).toHaveText('supervised');
// Execute skill
await page.click('[data-testid="execute-skill-button"]');
await page.selectOption('[name="skill"]', 'reconcile_inventory');
await page.fill('[name="task-input"]', 'Reconcile SKU-123');
await page.click('[data-testid="run-skill"]');
// Wait for execution to complete
await page.waitForSelector('[data-testid="execution-success"]', { timeout: 30000 });
await expect(page.locator('[data-testid="execution-result"]')).toBeVisible();
// Verify episode was recorded
await page.click('[data-testid="episodes-tab"]');
await expect(page.locator('text=reconcile_inventory')).toBeVisible();
await expect(page.locator('[data-testid="episode-status"]')).toHaveText('success');
});
test('should enforce governance for high-risk actions', async ({ page }) => {
// Create student agent (low maturity)
await page.goto('/agents');
await page.click('[data-testid="create-agent"]');
await page.fill('[name="name"]', 'Student Agent');
await page.selectOption('[name="maturity"]', 'student');
await page.click('[data-testid="save-agent"]');
// Try to execute high-risk action (delete_code)
await page.click('[data-testid="execute-skill-button"]');
await page.selectOption('[name="skill"]', 'delete_code');
await page.fill('[name="task-input"]', 'Delete old backup files');
await page.click('[data-testid="run-skill"]');
// Verify governance blocks the action
await expect(page.locator('[data-testid="governance-error"]')).toBeVisible();
await expect(page.locator('text=Insufficient maturity')).toBeVisible();
await expect(page.locator('text=student agents cannot delete code')).toBeVisible();
});
test('should verify tenant isolation in agents list', async ({ browser }) => {
// Create two contexts (different tenants)
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// Login as tenant-1
await page1.goto('/login');
await page1.fill('[name="email"]', 'tenant1@example.com');
await page1.fill('[name="password"]', 'password1');
await page1.click('[type="submit"]');
// Create agent as tenant-1
await page1.goto('/agents');
await page1.click('[data-testid="create-agent"]');
await page1.fill('[name="name"]', 'Tenant 1 Agent');
await page1.click('[data-testid="save-agent"]');
// Login as tenant-2
await page2.goto('/login');
await page2.fill('[name="email"]', 'tenant2@example.com');
await page2.fill('[name="password"]', 'password2');
await page2.click('[type="submit"]');
// Navigate to agents page as tenant-2
await page2.goto('/agents');
// Verify tenant-2 cannot see tenant-1's agent
await expect(page2.locator('text=Tenant 1 Agent')).not.toBeVisible();
// Cleanup
await context1.close();
await context2.close();
});
});---
5. Test Fixtures Guide
Fixtures provide reusable test data with sensible defaults.
Frontend Fixtures (TypeScript)
// src/__tests__/fixtures/agents.ts
/**
* Agent test fixtures
*/
import type { Agent, MaturityLevel } from '@/lib/ai/agent-governance';
export function createTestAgent(overrides: Partial<Agent> = {}): Agent {
const defaults: Agent = {
id: 'agent-123',
tenant_id: 'tenant-456',
name: 'Test Agent',
maturity_level: 'student' as MaturityLevel,
category: 'general',
confidence_score: 0.5,
created_at: new Date(),
updated_at: new Date(),
};
return { ...defaults, ...overrides };
}
export function createTestAgentList(count: number = 3): Agent[] {
return Array.from({ length: count }, (_, i) =>
createTestAgent({
id: `agent-${i + 1}`,
name: `Test Agent ${i + 1}`,
})
);
}Backend Fixtures (Python)
# backend-saas/tests/factories.py
"""
Test data factories for backend tests.
"""
from core.models import Agent, Tenant, Episode
from datetime import datetime
from typing import Dict, Any
import uuid
def create_test_agent(**overrides) -> Dict[str, Any]:
"""
Factory function for test agents with sensible defaults.
Usage:
agent = create_test_agent(maturity_level='autonomous')
agent = create_test_agent(name='Custom Name', category='finance')
"""
defaults = {
'id': str(uuid.uuid4()),
'tenant_id': 'tenant-123',
'name': 'Test Agent',
'maturity_level': 'student',
'category': 'general',
'confidence_score': 0.5,
'created_at': datetime.utcnow(),
'updated_at': datetime.utcnow(),
}
return {**defaults, **overrides}
def create_test_tenant(**overrides) -> Dict[str, Any]:
"""
Factory function for test tenants.
"""
defaults = {
'id': str(uuid.uuid4()),
'subdomain': 'testcompany',
'plan_tier': 'standard',
'created_at': datetime.utcnow(),
}
return {**defaults, **overrides}
def create_test_episode(**overrides) -> Dict[str, Any]:
"""
Factory function for test episodes.
"""
defaults = {
'id': str(uuid.uuid4()),
'agent_id': 'agent-123',
'tenant_id': 'tenant-456',
'task_description': 'Test task',
'outcome': 'success',
'success': True,
'constitutional_violations': [],
'zero_intervention': True,
'constitutional_score': 95.0,
'confidence_score': 0.9,
'created_at': datetime.utcnow(),
}
return {**defaults, **overrides}
def create_test_episode_list(count: int = 10, **overrides) -> list:
"""
Factory function for creating multiple test episodes.
"""
base_time = datetime.utcnow()
episodes = []
for i in range(count):
episode = create_test_episode(
id=f'episode-{i + 1}',
task_description=f'Test task {i + 1}',
created_at=base_time.replace(hour=i) # Different times
)
episodes.append(episode)
return episodesUsing Fixtures in Tests
# backend-saas/tests/unit/test_episode_service.py
from tests.factories import create_test_agent, create_test_episode, create_test_episode_list
class TestEpisodeService:
def test_recall_episodes_returns_recent(self, episode_service):
"""Given: 10 episodes created over 10 hours
When: Recalling last 5 episodes
Then: Should return 5 most recent episodes
"""
# Arrange - use factory
episodes = create_test_episode_list(count=10)
for episode in episodes:
episode_service.save(episode)
# Act
recalled = episode_service.recall(agent_id='agent-123', limit=5)
# Assert
assert len(recalled) == 5
# Verify most recent (highest hour value)
assert recalled[0].task_description == 'Test task 10'
assert recalled[4].task_description == 'Test task 6'---
6. Mock Strategy Guide
When to Mock vs. Real Implementations
// ✅ Mock: External APIs (LLM providers, OAuth)
it('should call OpenAI API with tenant key', async () => {
const mockFetch = vi.fn().mockResolvedValue({
json: async () => ({ choices: [{ message: { content: 'Test response' } }] })
});
global.fetch = mockFetch;
await llmRouter.call('tenant-123', { model: 'gpt-4o', messages: [] });
expect(mockFetch).toHaveBeenCalledWith(
'https://api.openai.com/v1/chat/completions',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer sk-test-tenant-key'
})
})
);
});
// ✅ Mock: Database queries in unit tests
it('should fetch agent from database', async () => {
vi.spyOn(db, 'query').mockResolvedValue({
rows: [{ id: 'agent-123', name: 'Test Agent' }]
});
const agent = await getAgent('agent-123');
expect(agent).toEqual({ id: 'agent-123', name: 'Test Agent' });
});
// ❌ Don't Mock: Database integration tests
// Use real SQLite in-memory database instead
// ❌ Don't Mock: Simple utility functions
// Test the real implementation
it('should calculate readiness score correctly', () => {
const readiness = calculateReadiness({
zeroInterventionRatio: 0.8,
avgConstitutionalScore: 95,
avgConfidenceScore: 0.9,
successRate: 0.95
});
expect(readiness).toBeCloseTo(0.862, 3); // Test real calculation
});Mock Patterns for External APIs
// Mocking LLM providers
vi.mocked(llmRouter.call).mockResolvedValue({
choices: [{ message: { role: 'assistant', content: 'Test response' } }],
usage: { total_tokens: 100 }
});
// Mocking OAuth providers
nock('https://github.com')
.post('/login/oauth/access_token')
.reply(200, {
access_token: 'ghp-test-token',
token_type: 'bearer'
});
// Mocking ATOM Cloud CLI
vi.spyOn(child_process, 'exec').mockImplementation((cmd, callback) => {
if (cmd.includes('atom-cli nodes create')) {
callback(null, { stdout: 'Node created: test-node' });
}
});---
Summary
This guide provides templates and examples for writing tests across the ATOM SaaS platform:
**Frontend (Vitest + React Testing Library):**
- Unit tests for services, components, API routes
- Mock vi.fn() for dependencies
- React Testing Library for component testing
**Backend (pytest):**
- Unit tests for services, models, API routes
- Fixtures for reusable test data
- Mock patches for external dependencies
**Integration Tests:**
- Service + database interactions
- Mock external APIs (atom-cli, OAuth)
- Test complete workflows
**E2E Tests (Playwright):**
- Complete user journeys
- Multi-page workflows
- Tenant isolation verification
**Next Steps:**
- Use templates as starting point for new tests
- Adapt examples to your specific feature
- Follow platform patterns (tenant isolation, BYOK, governance)
- Run tests locally before committing
---
*Document version: 1.0*
*Last updated: 2026-02-22*
*Maintained by: Platform Engineering Team*